/*
 * Copyright 2010 - Jeremie Brebec
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package org.jb.statemachine.server.impl;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;

import org.jb.notification.client.Notification;
import org.jb.notification.server.NotificationManager;
import org.jb.statemachine.server.InitialState;
import org.jb.statemachine.server.Message;
import org.jb.statemachine.server.StateMachine;
import org.jb.statemachine.server.Transition;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.inject.Inject;

/*
 * 
 */
public class StateMachineImpl implements StateMachine {

	private final Logger logger = LoggerFactory.getLogger(StateMachineImpl.class);

	@Inject
	public StateMachineImpl( @InitialState Object state, Set<Transition> transitions ) {
		setState( state );
		
		for( Transition t : transitions ) {
			registerTransition( t );
		}
	}
	
	/*
	 * (non-Javadoc)
	 * @see org.jb.coinche.server.statemachine.StateMachine#processMessage(java.lang.Object)
	 */
	@Override
	@Message
	synchronized public void processMessage(Object message) throws Throwable {
		
		logger.debug("Processing message... {}", message );
		sendMessageToState( message );
		
		processMessageWithTransitions( message );
		
	}
	
	/**
	 * 
	 * @param notificationManager
	 */
	@Inject
	public void setNotificationManager( NotificationManager notificationManager ) {
		this.notificationManager = notificationManager;
				
		if (state != null) {
			this.lastcheckpoint = notificationManager.getNextId();
			notificationManager.wakeup();
		}
	}

	/*
	 * (non-Javadoc)
	 * @see org.jb.coinche.server.statemachine.StateMachine#registerTransition(org.jb.coinche.server.statemachine.Transition)
	 */
	@Override
	public void registerTransition(Transition transition) {
		transitions.add(transition);
	}
	
	/*
	 * 
	 */
	synchronized public void setState( Object state ) {
		
		logger.debug("Setting new state to {}", state );
		
		this.state = state;
				
		if (notificationManager != null) {
			this.lastcheckpoint = notificationManager.getNextId();
			notificationManager.wakeup();
		}
	}
		
	/*
	 * 
	 */
	@SuppressWarnings("unchecked")
	public <T>  T getState() {
		return (T)state;
	}

	/*
	 * 
	 */
	private boolean sendMessageToState( Object message ) throws Throwable {
		if (state == null)
			return false;

		Method[] methods = state.getClass().getMethods();
		if (methods == null || methods.length == 0)
			return false;
		
		for( Method method : methods ) {
			Message m = method.getAnnotation(Message.class);
			if (m == null)
				continue;
			
			Class<?>[] parameters = method.getParameterTypes();
			if (parameters == null || parameters.length == 0)
				continue;
			
			if (parameters.length != 1 )
				continue;
			
			Class<?> c = parameters[0];
			if (c.isAssignableFrom(message.getClass()))
			{
				try {
					logger.debug("Sending message to state {} with method {}", state.getClass().getSimpleName(), method.getName() );
					Object r = method.invoke( state, message );
					if (r != null) {
						setState( r );
						return true;
					}
				}
				catch( InvocationTargetException e ) {
					throw e.getTargetException();
				}
				catch( Exception e ) {
					e.printStackTrace();
				}
			}
		}
		
		return false;
	}
	
	/*
	 * 
	 */
	private boolean processMessageWithTransitions( Object message ) throws Throwable {
		for( Transition transition : transitions ) {
			
			Method[] methods = transition.getClass().getMethods();
			if (methods == null || methods.length == 0)
				continue;
			
			for( Method method : methods ) {
				Message m = method.getAnnotation(Message.class);
				if (m == null)
					continue;
				
				Class<?>[] parameters = method.getParameterTypes();
				if (parameters == null || parameters.length == 0)
					continue;
				
				if (parameters.length == 1 ) {
					/*
					 * 1 parametre -> state
					 */
					
					Class<?> clazzMsg = parameters[0];
					if (!clazzMsg.isAssignableFrom(state.getClass()))
						continue;
					
					try {
						logger.debug("Sending message to transition {} for state {} with method {}", new Object[]{ transition.getClass().getSimpleName(), state.getClass().getSimpleName(), method.getName() } );

						Object r = method.invoke( transition, state );
						if (r != null) {
							setState( r );
							return true;
						}
					}
					catch( InvocationTargetException e ) {
						throw e.getTargetException();
					}
					catch( Exception e ) {
						e.printStackTrace();
					}
					
				} else if (parameters.length == 2 ){
					/*
					 * 2 parametres -> state ; message
					 */
					if (state == null)
						continue;
						
					Class<?> clazzState = parameters[0];
					if (!clazzState.isAssignableFrom(state.getClass()))
						continue;
					
					Class<?> clazzMsg = parameters[1];
					if (!clazzMsg.isAssignableFrom(message.getClass()))
						continue;
					
					try {
						logger.debug("Sending message to transition {} for state {} and message {} with method {}", new Object[]{ transition.getClass().getSimpleName(), state.getClass().getSimpleName(), message.getClass().getSimpleName(), method.getName() } );

						Object r = method.invoke( transition, state, message );
						if (r != null) {
							setState( r );
							return true;
						}
					}
					catch( InvocationTargetException e ) {
						throw e.getTargetException();
					}
					catch( Exception e ) {
						e.printStackTrace();
					}
				}
			}
		}
		
		return false;
	}
	
	/*
	 * (non-Javadoc)
	 * @see org.jb.coinche.server.notify.NotificationProvider#notify(java.util.Collection, java.util.Date)
	 */
	@Override
	synchronized public void notify(Collection<Notification> notifications, long fromCheckpoint, long toCheckpoint ) {
		if (fromCheckpoint < lastcheckpoint )
			notifications.add( this.<Notification>getState() );
	}
	
	/*
	 * 
	 */
	private List<Transition> transitions = new ArrayList<Transition>();
	
	private Object state = null;
		
	private long lastcheckpoint = 0;
	
	private NotificationManager notificationManager;


}
